package org.geektimes.configuration.microprofile.config.converters;

import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.PriorityQueue;
import java.util.ServiceLoader;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

import org.eclipse.microprofile.config.spi.Converter;

@SuppressWarnings("rawtypes")
public class Converters implements Iterable<Converter> {

	private final static int DEFAULT_PRIORITY = 100;

	private ClassLoader classLoader;

	private boolean addedDiscoveredConverters;

	private ConcurrentHashMap<Class, PriorityQueue<PrioritizedConverter>> typeConverters = new ConcurrentHashMap<Class, PriorityQueue<PrioritizedConverter>>();

	public Converters() {
		this(Thread.currentThread().getContextClassLoader());
	}

	public Converters(ClassLoader classLoader) {
		this.classLoader = classLoader;
	}

	public void setClassLoader(ClassLoader classLoader) {
		this.classLoader = classLoader;
	}

	public void addDiscovertedConverters() {

		if (addedDiscoveredConverters)
			return;
		addConverters(ServiceLoader.load(Converter.class, classLoader));
		addedDiscoveredConverters = true;

	}

	public void addConverter(Converter converter) {
		addConverter(converter, DEFAULT_PRIORITY);
	}

	public void addConverter(Converter converter, Class type) {
		addConverter(converter, type, DEFAULT_PRIORITY);
	}

	public void addConverter(Converter converter, int priority) {
		addConverter(converter, resolveConvertedType(converter), priority);
	}

	@SuppressWarnings("unchecked")
	public void addConverter(Converter converter, Class type, int priority) {
		PriorityQueue priorityQueue = this.typeConverters.computeIfAbsent(type, t -> new PriorityQueue());
		priorityQueue.add(new PrioritizedConverter(converter, priority));
	}

	public void addConverters(Iterable<Converter> converters) {
		if (converters != null)
			converters.forEach(this::addConverter);
	}

	public void addConverters(Converter... converters) {
		addConverters(Arrays.asList(converters));
	}

	@Override
	public Iterator<Converter> iterator() {
		return typeConverters.values().stream().flatMap(p -> p.stream()).map(p -> (Converter) p)
				.collect(Collectors.toList()).iterator();
	}

	public List<Converter> getConverters(Class convertType) {

		PriorityQueue<PrioritizedConverter> prioritizedConverters = typeConverters.get(convertType);
		if (prioritizedConverters.isEmpty()) {
			return Collections.emptyList();
		}
		List<Converter> converters = new ArrayList<Converter>();
		for (PrioritizedConverter prioritizedConverter : prioritizedConverters) {
			converters.add(prioritizedConverter.getConverter());
		}
		return converters;

	}

	private Class resolveConvertedType(Converter converter) {
		assertConverter(converter);

		Class convertedType = null;
		Class converterClass = converter.getClass();

		while (converterClass != null) {
			convertedType = resolveConvertedType(converterClass);
			if (convertedType != null)
				break;
			convertedType = resolveConvertedType(converterClass.getGenericSuperclass());
			if (convertedType != null)
				break;
			converterClass = converterClass.getSuperclass();
		}

		return convertedType;
	}

	private Class resolveConvertedType(Class clazz) {
		Class convertedType = null;
		Type[] genericInterfaces = clazz.getGenericInterfaces();
		for (Type genericInterface : genericInterfaces) {
			if ((convertedType = resolveConvertedType(genericInterface)) != null) {
				break;
			}
		}
		return convertedType;

	}

	private Class resolveConvertedType(Type type) {
		Class convertedType = null;
		if (type instanceof ParameterizedType) {
			ParameterizedType parameterizedType = (ParameterizedType) type;
			if (parameterizedType.getRawType() instanceof Class) {
				if (Converter.class.isAssignableFrom((Class) parameterizedType.getRawType())) {
					Type[] actualTypes = parameterizedType.getActualTypeArguments();
					if (actualTypes.length == 1 && actualTypes[0] instanceof Class) {
						convertedType = (Class) actualTypes[0];
					}
				}
			}
		}
		return convertedType;
	}

	private void assertConverter(Converter converter) {

		if (converter.getClass().isInterface()) {
			throw new IllegalArgumentException("Converter must not be interface!");
		}

		if (Modifier.isAbstract(converter.getClass().getModifiers())) {
			throw new IllegalArgumentException("Converter must not be abstract!");
		}
	}

}
